Some general remarks about the exercises:
Cell > Cell Type > Markdown). But make sure to replace any part that says YOUR CODE HERE or YOUR ANSWER HERE and remove the raise NotImplementedError().Kernel > Restart & Run All in the jupyter menu.assert statements. When run, a successful test will give no output, whereas a failed test will display an error message.Please fill in your name here:
NAME = "Kees van Kempen"
NAMES_OF_COLLABORATORS = "Bart Steeman" # We did not finish it together.
Exercise sheet 6
Code from the lecture
import numpy as np
import matplotlib.pylab as plt
rng = np.random.default_rng()
%matplotlib inline
def aligned_init_config(width):
'''Produce an all +1 configuration.'''
return np.ones((width,width),dtype=int)
def plot_ising(config,ax,title):
'''Plot the configuration.'''
ax.matshow(config, vmin=-1, vmax=1, cmap=plt.cm.binary)
ax.title.set_text(title)
ax.set_yticklabels([])
ax.set_xticklabels([])
ax.set_yticks([])
ax.set_xticks([])
from collections import deque
def neighboring_sites(s,w):
'''Return the coordinates of the 4 sites adjacent to s on an w*w lattice.'''
return [((s[0]+1)%w,s[1]),((s[0]-1)%w,s[1]),(s[0],(s[1]+1)%w),(s[0],(s[1]-1)%w)]
def cluster_flip(state,seed,p_add):
'''Perform a single Wolff cluster move with specified seed on the state with parameter p_add.'''
w = len(state)
spin = state[seed]
state[seed] = -spin
cluster_size = 1
unvisited = deque([seed]) # use a deque to efficiently track the unvisited cluster sites
while unvisited: # while unvisited sites remain
site = unvisited.pop() # take one and remove from the unvisited list
for nbr in neighboring_sites(site,w):
if state[nbr] == spin and rng.uniform() < p_add:
state[nbr] = -spin
unvisited.appendleft(nbr)
cluster_size += 1
return cluster_size
def wolff_cluster_move(state,p_add):
'''Perform a single Wolff cluster move on the state with addition probability p_add.'''
seed = tuple(rng.integers(0,len(state),2))
return cluster_flip(state,seed,p_add)
def compute_magnetization(config):
'''Compute the magnetization M(s) of the state config.'''
return np.sum(config)
def run_ising_wolff_mcmc(state,p_add,n):
'''Run n Wolff moves on state and return total number of spins flipped.'''
total = 0
for _ in range(n):
total += wolff_cluster_move(state,p_add)
return total
def sample_autocovariance(x,tmax):
'''Compute the autocorrelation of the time series x for t = 0,1,...,tmax-1.'''
x_shifted = x - np.mean(x)
return np.array([np.dot(x_shifted[:len(x)-t],x_shifted[t:])/len(x)
for t in range(tmax)])
def find_correlation_time(autocov):
'''Return the index of the first entry that is smaller than
autocov[0]/e or the length of autocov if none are smaller.'''
smaller = np.where(autocov < np.exp(-1)*autocov[0])[0]
return smaller[0] if len(smaller) > 0 else len(autocov)
(100 Points)
Goal of this exercise: Practice implementing MCMC simulation of the XY spin model using an appropriate cluster algorithm and analyzing the numerical effectiveness.
The XY model is a relative of the Ising model in which the discrete spin at each lattice site is replaced by a continuous -dimensional spin on the unit circle
To be precise, we consider a lattice with periodic boundary conditions and a XY configuration , , with Hamiltonian that is very similar to the Ising model,
Here, as in the Ising model, the sum runs over nearest neighbor pairs and and is the usual Euclidean inner product of the vectors . We will only consider the ferromagnetic XY model and set in the remainder of the exercise. Note that nowhere in the definition the - and -components of the spins are related to the two directions of the lattice (one could also have studied the XY model on a one-dimensional or three-dimensional lattice and the spins would still have two components). As usual we are interested in sampling configurations with distribution given by the Boltzmann distribution
The XY model admits a (local) cluster algorithm that is very similar to the Wolff algorithm of the Ising model. It amounts to the following recipe:
(a) Verify by a calculation, to be included using markdown and LaTeX below, that the probabilities are the appropriate ones to ensure detailed balance for the Boltzmann distribution . Hint: Follow the same reasoning as for the Ising model. Compare the probabilities involved in producing the cluster in state and state . Why do the probabilities only differ at the boundary edges in the cluster ? (25 pts)
The condition for detailed balance is Let's work to that expression by following the idea of the proof for the Ising model proof.
Starting from the left-hand side, consider the same cluster from the same seed and unit vector but between different states and . Flipping between and is done by the same cluster , but from opposite sign. The probability of this happening is given by the boundaries of the clusters, namely that they are not added to the cluster, with probability per boundary site. This gives a ratio for pairs with and neighbouring just outside the cluster with . Substituting the formula for in, we find with describing both types of pairs.
For the right-hand side, just from the definition of . This difference in energy we can write as by summing over all pairs as above, as pairs not across the boundary do not give rise to an energy change. The last step is due to the flips from both signs to be accounted for. This shows thus that detailed balance holds.
(b) In order to implement the cluster update described above, we take the state to be described by a Numpy array of dimension , for which we have already provided a function xy_aligned_init_config to generate an all-aligned initial state. Write the function xy_cluster_flip, that grows and flips a cluster starting from the given seed site and and returns the cluster size, and xy_cluster_move, that performs the previous function to a random seed and direction and also returns the cluster size. (20 pts)
def xy_aligned_init_config(width):
'''Return an array of dimension (width,width,2) representing aligned spins in x-direction.'''
return np.dstack((np.ones((width,width)),np.zeros((width,width))))
def xy_cluster_flip(state,seed,nhat,beta):
'''Perform a cluster move with specified seed and vector nhat on the state at temperature beta.'''
w = len(state)
# Let's flip along the way, starting with the seed.
state[seed] -= 2*state[seed]@nhat*nhat
# Use a double-ended queue to keep track of what elements to try to expand from.
unvisited = deque([seed])
cluster_size = 1
while unvisited:
site = unvisited.pop()
# As the site is already added to the queue, it has already been flipped
# so we need to take a minus into account.
s_i_n = -state[site]@nhat
for nbr in neighboring_sites(site,w):
s_j_n = state[nbr]@nhat
one_minus_p_add = np.exp(-2*beta*s_i_n*s_j_n)
if np.sign(s_i_n) == np.sign(s_j_n) and rng.uniform() > one_minus_p_add:
state[nbr] -= 2*s_j_n*nhat
unvisited.appendleft(nbr)
cluster_size += 1
return cluster_size
def xy_cluster_move(state,beta):
'''Perform a single Wolff cluster move on the state with addition probability p_add.'''
seed = tuple(rng.integers(0,len(state),2))
phi = 2*np.pi*rng.uniform()
nhat = np.array([np.cos(phi), np.sin(phi)])
return xy_cluster_flip(state,seed,nhat,beta)
cell-2e1ebc198d10ea5b
Score: 15.0 / 15.0 (Top)
from nose.tools import assert_almost_equal
assert 1 <= xy_cluster_flip(xy_aligned_init_config(4),(0,0),np.array([np.cos(0.5),np.sin(0.5)]),0.5) <= 16
assert_almost_equal(np.mean([xy_cluster_flip(xy_aligned_init_config(3),(0,0),
np.array([np.cos(0.5),np.sin(0.5)]),0.3)
for _ in range(200)]),5.3,delta=0.7)
assert_almost_equal(np.mean([xy_cluster_flip(xy_aligned_init_config(3),(1,2),
np.array([np.cos(0.2),np.sin(0.2)]),0.2)
for _ in range(200)]),4.3,delta=0.6)
cell-9445d8ff6dc2a16f
Score: 5.0 / 5.0 (Top)
assert 1 <= xy_cluster_move(xy_aligned_init_config(4),0.5) <= 16
assert_almost_equal(np.mean([xy_cluster_move(xy_aligned_init_config(3),0.3)
for _ in range(200)]),3.6,delta=0.75)
assert_almost_equal(np.mean([xy_cluster_move(xy_aligned_init_config(3),0.9)
for _ in range(200)]),6.3,delta=0.75)
(c) Estimate and plot the average cluster size in equilibrium for a 25x25 lattice () for the range of temperatures .
It is not necessary first to estimate the equilibration time: you may
start in a fully aligned state and use 400 moves for equilibration and
1000 for estimating the average cluster size. It is not necessary to
estimate errors for this average. Store your averages in the data set "cluster-size" (an array of size 11) in the HDF5-file xy_data.hdf5, just like you did in Exercise sheet 5. Then read the data from file and produce a plot. (20 pts)
temperatures = np.linspace(0.5,1.5,11)
width = 25
equilibration_moves = 400
measurement_moves = 1000
import h5py
import time
t_0 = time.time()
def toc():
"""Return how many seconds the function was last called."""
if not 't_0' in globals():
return -np.inf
global t_0
dt = time.time() - t_0
t_0 = time.time()
return dt
with h5py.File("xy_data.hdf5", "a") as f:
if not "cluster-size" in f:
state = xy_aligned_init_config(width)
cluster_sizes = np.zeros(len(temperatures))
for idx, T in enumerate(temperatures):
toc()
print("T = {:.1f}".format(T))
beta = 1/T
# Equilibrate
for _ in range(equilibration_moves):
xy_cluster_move(state,beta)
print("\tequilibrating took {:2.3f} seconds".format(toc()))
# Measure
for _ in range(measurement_moves):
cluster_sizes[idx] += xy_cluster_move(state,beta)
cluster_sizes[idx] /= measurement_moves
print("\tmeasuring took {:2.3f} seconds".format(toc()))
print()
f.create_dataset("cluster-size",data=cluster_sizes)
cell-69f351f86b801f63
Score: 10.0 / 10.0 (Top)
with h5py.File('xy_data.hdf5','r') as f:
assert f["cluster-size"][()].shape == (11,)
assert_almost_equal(f["cluster-size"][4],225,delta=40)
assert_almost_equal(f["cluster-size"][10],8,delta=8)
# Plotting
with h5py.File("xy_data.hdf5", "r") as f:
plt.figure()
# My girlfriend told me to use this marker.
plt.plot(temperatures, np.array(f["cluster-size"]), marker="4", markersize=20, linestyle="dashed", linewidth=.5)
plt.xlabel("temperature $T$")
plt.ylabel("average cluster size")
plt.title("Average cluster size for the $w \\times w$ XY model \n"
"(w = {}, #equilibrations = {}, #measurements = {})"
.format(width, equilibration_moves, measurement_moves))
plt.show()
(d) Make an MCMC estimate (and plot!) of the mean square magnetization per spin for the same set of temperatures, where
To choose the equilibration time and time between
measurement, use the average cluster size from (c) to estimate how many
moves correspond to 1 sweep, i.e. roughly updates to spins. Then use 100 equilibration sweeps and 200 measurements of , with 2 sweeps between each measurement. Store the measured values of in the data set "square-magn" of dimension in xy_data.hdf5.
Then read the data and plot estimates for
including errors (based on batching or jackknife). If the errors are
too small to see, you may multiply them by some number and indicate this
in the title of the plot. (20 pts)
measurements = 200
equil_sweeps = 100
measure_sweeps = 2
with h5py.File("xy_data.hdf5", "a") as f:
if not "square-magn" in f:
state = xy_aligned_init_config(width)
square_magn = np.zeros((len(temperatures), measurements))
for idx, T in enumerate(temperatures):
toc()
print("T = {:.1f}".format(T))
beta = 1/T
# We let the sweep size depend on the width and the average cluster size
# from before:
sweep_size = np.ceil(width**2/f["cluster-size"][idx]).astype(int)
# Equilibrate
for j in range(equil_sweeps*sweep_size):
xy_cluster_move(state,beta)
print("\tequilibrating... [{}/{}]".format(j, equil_sweeps*sweep_size), end='\r')
print("\tequilibrating took {:2.3f} seconds".format(toc()))
# Measure
for j in range(measurements):
xy_cluster_move(state,beta)
square_magn[idx][j] = np.linalg.norm(np.mean(state, axis=(0,1)))**2
print("\tmeasuring... [{}/{}]".format(j, measurements), end='\r')
# Sweeps between measurements
for _ in range(measure_sweeps*sweep_size):
xy_cluster_move(state,beta)
print("\tmeasuring took {:2.3f} seconds".format(toc()))
print()
f.create_dataset("square-magn",data=square_magn)
cell-5d3a2f49615613b2
Score: 10.0 / 10.0 (Top)
with h5py.File('xy_data.hdf5','r') as f:
assert f["square-magn"][()].shape == (11, 200)
assert_almost_equal(np.mean(f["square-magn"][4]),0.456,delta=0.02)
assert_almost_equal(np.mean(f["square-magn"][9]),0.023,delta=0.01)
# Plotting
# As copied from last week:
def jackknife_batch_estimate(data,observable,k):
'''Divide data into k batches and apply the function observable to each
collection of all but one batches. Returns the mean and corrected
standard error.'''
batches = np.reshape(data,(k,-1))
values = [observable(np.delete(batches,i,0).flatten()) for i in range(k)]
return np.mean(values), np.sqrt(k-1)*np.std(values)
with h5py.File('xy_data.hdf5','r') as f:
k = 100
mean_sq, std_sq = np.array([jackknife_batch_estimate(data, np.mean, k) for data in f["square-magn"]]).T
plt.figure()
# I do plot a line to visualize the trend.
plt.errorbar(temperatures, mean_sq, yerr=10*std_sq,
fmt='.--', markersize=10, capsize=4)
plt.xlabel("temperature $T$")
plt.ylabel("mean square magnetization $\\overline{m^2}$")
plt.title("Mean square magnetization for the $w \\times w$ XY model \n"
"(w = {}, #measurements = {}, $10\\sigma$ errors)"
.format(width, measurements))
plt.show()
(e) Produce a single equilibrated state for each temperature and store them in the data set "states" of dimension in xy_data.hdf5. Then read them and produce a table of plots using the provided function plot_xy, which shows colors based on the angle of the spin, each with a title to indicate the temperature. Can you observe the Kosterlitz–Thouless transition of the XY model? (15 pts)
width = 25
state = xy_aligned_init_config(width)
equil_sweeps = 200
with h5py.File("xy_data.hdf5", "a") as f:
if not "states" in f:
states = np.zeros(temperatures.shape + state.shape)
for idx, T in enumerate(temperatures):
toc()
print("T = {:.1f}".format(T))
beta = 1/T
sweep_size = np.ceil(width**2/f["cluster-size"][idx]).astype(int)
# Equilibrate
for j in range(equil_sweeps*sweep_size):
xy_cluster_move(state,beta)
print("\tequilibrating... [{}/{}]".format(j, equil_sweeps*sweep_size), end='\r')
print("\tequilibrating took {:2.3f} seconds".format(toc()))
states[idx] = state.copy()
print()
f.create_dataset("states", data=states)
cell-8968d7e317e522f7
Score: 5.0 / 5.0 (Top)
with h5py.File('xy_data.hdf5','r') as f:
assert f["states"][()].shape == (11, 25, 25, 2)
def plot_xy(state,ax,title=""):
'''Plot the XY configuration given by state. Takes an Axes object ax from
matplotlib to draw to, and adds the specified title.'''
angles = np.arctan2(*np.transpose(state,axes=(2,0,1)))
ax.matshow(angles, vmin=-np.pi, vmax=np.pi, cmap=plt.cm.hsv)
ax.title.set_text(title)
ax.set_yticklabels([])
ax.set_xticklabels([])
ax.set_yticks([])
ax.set_xticks([])
# Make a table of plots
with h5py.File("xy_data.hdf5", "a") as f:
fig, axs = plt.subplots(3, 4, figsize=(10, 10))
axs = axs.ravel()
for idx, T in enumerate(temperatures):
plot_xy(f["states"][idx], axs[idx],"T = {:.1f}".format(T))
# Hide the axes of unused plots.
[ax.axis('off') for ax in axs[len(temperatures):]]
plt.tight_layout()
plt.show()
The Kosterlitz-Thouless transition is vaguely visible. For and lower, vortices and anti-vortices can be recognized. Above this temperature, this structure vanishes.